﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;

using FlickrNet;
using FlickrApi = FlickrNet.Flickr;

using Yorrick.FlickrMetadataSynchr.Events;
using Yorrick.FlickrMetadataSynchr.Properties;

namespace Yorrick.FlickrMetadataSynchr.Flickr
{
    /// <summary>
    /// Abstract class that implements the machinery to asynchronously process metadata on Flickr. This can either be fetching metadata
    /// or updating metadata.
    /// </summary>
    /// <typeparam name="TIn">The type of metadata that is processed.</typeparam>
    /// <typeparam name="TOut">The type of metadata that is the result of the processing.</typeparam>
    internal abstract class AsyncFlickrWorker<TIn, TOut>
    {
        #region Private fields
        /// <summary>
        /// The name to assign to the background worker thread that is used.
        /// </summary>
        private string _workerThreadName;

        /// <summary>
        /// The worker thread to use for processing the metadata on Flickr.
        /// </summary>
        private Thread _workerThread;

        /// <summary>
        /// The list of metadata instances that should be processed on Flickr.
        /// </summary>
        private IList<TIn> _metadataList;

        /// <summary>
        /// The callback for the asynchronous operation to report the result.
        /// </summary>
        private EventHandler<AsyncFlickrWorkerEventArgs<TOut>> _resultCallback;

        /// <summary>
        /// Whether or not the asynchronous update process has started.
        /// </summary>
        private bool _hasStarted;

        /// <summary>
        /// Object to lock on when modifying internal state.
        /// </summary>
        private object _internalStateLock = new Object();

        /// <summary>
        /// The internal wait handle for synchronization purposes.
        /// </summary>
        private ManualResetEvent _manualResetEvent;
        #endregion

        #region Protected properties
        /// <summary>
        /// The FlickrNet API object.
        /// </summary>
        protected FlickrApi FlickrApi
        {
            get;
            private set;
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Constructs an instance of the asynchronous worker.
        /// </summary>
        /// <param name="flickrApi">The FlickrNet API object to use.</param>
        /// <param name="workerThreadName">The name to assign to the background worker thread that is used.</param>
        protected AsyncFlickrWorker(FlickrApi flickrApi, string workerThreadName)
        {
            if (flickrApi == null)
            {
                throw new ArgumentNullException("flickrApi");
            }

            FlickrApi = flickrApi;
            _workerThreadName = workerThreadName;
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Starts the async process. This method should not be called when the asychronous process is already in progress.
        /// </summary>
        /// <param name="metadataList">The list with <typeparamref name="TIn"/> instances of metadata that should
        /// be processed on Flickr.</param>
        /// <param name="resultCallback">A callback that receives the result. Is not allowed to be null.</param>
        /// <typeparam name="TIn">The type of metadata that is processed.</typeparam>
        /// <typeparam name="TOut">The type of metadata that is the result of the processing.</typeparam>
        /// <returns>Returns a <see cref="WaitHandle"/> that can be used for synchronization purposes. It will be signaled when
        /// the async process is done.</returns>
        public WaitHandle BeginWork(IList<TIn> metadataList, EventHandler<AsyncFlickrWorkerEventArgs<TOut>> resultCallback)
        {
            if (metadataList == null)
            {
                throw new ArgumentNullException("metadataList");
            }
            if (resultCallback == null)
            {
                throw new ArgumentNullException("resultCallback");
            }

            lock (_internalStateLock)
            {
                if (_hasStarted)
                {
                    throw new InvalidOperationException(Messages.AsyncFlickrWorkerAlreadyStarted);
                }

                _hasStarted = true;

                _manualResetEvent = new ManualResetEvent(false);

                _metadataList = metadataList;
                _resultCallback = resultCallback;

                _workerThread = new Thread(StartWork);
                _workerThread.IsBackground = true;
                _workerThread.Name = _workerThreadName;
                _workerThread.Start();

                return _manualResetEvent;
            }
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Method that does the asynchronous work on a background thread. Reports results through a callback method.
        /// </summary>
        private void StartWork()
        {
            int sequenceNumber = 1;

            foreach (TIn metadata in _metadataList)
            {
                AsyncFlickrWorkerEventArgs<TOut> eventArgs;

                try
                {
                    TOut metadataResult = ProcessItem(metadata);

                    eventArgs = new AsyncFlickrWorkerEventArgs<TOut>(metadataResult, sequenceNumber);
                }
                catch (FlickrException ex)
                {
                    eventArgs = new AsyncFlickrWorkerEventArgs<TOut>(GetMetadataInCaseOfError(metadata), ex, sequenceNumber);
                }
                catch (WebException ex)
                {
                    Exception ex2 = FlickrHelperException.GetFailedToConnectToFlickrException(ex);

                    eventArgs = new AsyncFlickrWorkerEventArgs<TOut>(GetMetadataInCaseOfError(metadata), ex2, sequenceNumber);
                }

                _resultCallback(this, eventArgs);

                if (eventArgs.Cancel)
                {
                    break;
                }

                sequenceNumber++;
            }

            CleanUpAfterFinishOrAbort();
        }

        /// <summary>
        /// Cleans up internal state after the async process has finished or is aborted.
        /// </summary>
        private void CleanUpAfterFinishOrAbort()
        {
            lock (_internalStateLock)
            {
                _resultCallback = null;
                _metadataList = null;
                _workerThread = null;
                _hasStarted = false;
                _manualResetEvent.Set();
                _manualResetEvent = null;
            }
        }
        #endregion

        #region Protected methods
        /// <summary>
        /// Processes a metadata item and returns the metadata result.
        /// </summary>
        /// <param name="metadata">The metadata item to process.</param>
        /// <returns>The metadata result.</returns>
        protected abstract TOut ProcessItem(TIn metadata);

        /// <summary>
        /// Gets the metadata result to use when an error occurred.
        /// </summary>
        /// <param name="metadata">The metadata that was not processed successfully.</param>
        /// <typeparam name="TIn">The type of metadata that is processed.</typeparam>
        /// <typeparam name="TOut">The type of metadata that is the result of the processing.</typeparam>
        /// <returns>The metadata result.</returns>
        protected abstract TOut GetMetadataInCaseOfError(TIn metadata);
        #endregion

    }
}
